using System;
using System.Collections.Generic;
using Microsoft.Practices.ServiceLocation;
using NQueries.Utils;

namespace NQueries
{
    /// <inheritdoc />
    public sealed class UnitOfWork : IUnitOfWork
    {
        static readonly object _Lock = new object();

        [ThreadStatic]
        static Stack<IUnitOfWork> _UnitOfWorkQueue;

        readonly DateTime _CreateDate;
        readonly bool _IsDataContextOwner;
        bool _IsDisposed;

        UnitOfWork(string name, IDataContext dataContext)
        {
            Name = name;

            lock (_Lock)
            {
                if (_UnitOfWorkQueue == null)
                    _UnitOfWorkQueue = new Stack<IUnitOfWork>();
                _UnitOfWorkQueue.Push(this);
            }

            if (dataContext == null)
            {
                DataContext = ServiceLocator.Current.GetInstance<IDataContext>();
                _IsDataContextOwner = true;
            }
            else
                DataContext = dataContext;

            OnUnitOfWorkCreated?.Invoke(this, new UnitOfWorkCreatedEventArgs(Name));

            _CreateDate = DateTime.UtcNow;
        }

        /// <summary>
        /// Gets the current <see cref="IDataContext"/>.
        /// </summary>
        public static IDataContext CurrentDataContext
        {
            get
            {
                lock (_Lock)
                    return _UnitOfWorkQueue != null && _UnitOfWorkQueue.Count > 0 ? ((UnitOfWork)_UnitOfWorkQueue.Peek()).DataContext : null;
            }
        }

        /// <summary>
        /// Starts a new unit of work.
        /// </summary>
        /// <returns></returns>
        public static IUnitOfWork Begin() { return new UnitOfWork(string.Empty, null); }

        /// <summary>
        /// Starts a unit of work.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public static IUnitOfWork Begin(string name) { return new UnitOfWork(name, null); }

        /// <summary>
        /// Starts a new unit of work and tries to use a existing data context from the current thread.
        /// If no datacontext is present, a new one will be created.
        /// </summary>
        /// <returns></returns>
        public static IUnitOfWork BeginOrJoin()
        {
            lock (_Lock)
                return CurrentDataContext != null ? new UnitOfWork(string.Empty, CurrentDataContext) : Begin();
        }

        /// <summary>
        /// Starts a new unit of work and tries to use a existing data context from the current thread.
        /// If no datacontext is present, a new one will be created.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public static IUnitOfWork BeginOrJoin(string name)
        {
            lock (_Lock)
                return CurrentDataContext != null ? new UnitOfWork(name, CurrentDataContext) : Begin(name);
        }

        /// <inheritdoc />
        void Dispose(bool disposing)
        {
            if (!_IsDisposed)
            {
                if (disposing)
                {
                    if (_IsDataContextOwner)
                        DataContext?.Dispose();
                    DataContext = null;
                }

                lock (_Lock)
                {
                    _UnitOfWorkQueue?.Pop();
                }
                _IsDisposed = true;

                OnUnitOfWorkDisposed?.Invoke(this, new UnitOfWorkDisposedEventArgs(Name, DateTime.UtcNow - _CreateDate));
            }
        }

        /// <summary>
        /// Destructor.
        /// </summary>
        ~UnitOfWork() { Dispose(false); }

        /// <summary>
        /// Is called, whenever a unit of work is created.
        /// </summary>
        public static event EventHandler<UnitOfWorkCreatedEventArgs> OnUnitOfWorkCreated;

        /// <summary>
        /// Is called, just before a unit of work gets disposed.
        /// </summary>
        public static event EventHandler<UnitOfWorkDisposedEventArgs> OnUnitOfWorkDisposed;

        /// <inheritdoc />
        public override string ToString() { return "{0} name: <{1}>".FormatWith(base.ToString(), Name); }

        #region IUnitOfWork Members

        /// <summary>
        /// Disposes the object.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <inheritdoc />
        public IDataContext DataContext { get; private set; }

        /// <inheritdoc />
        public string Name { get; }

        /// <inheritdoc />
        public int Commit() { return DataContext.SaveChanges(); }

        #endregion
    }
}